﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using CommonServiceManager.Extensions;

namespace CommonServiceManager.Configuration
{
	/// <summary>
	/// Base class for <see cref="IConfigurator{TImplementation}"/> that implements the expression based members that end up calling their non-expression based counterparts.
	/// </summary>
	/// <typeparam name="TImplementation">The type to configure dependency injection for</typeparam>
	public abstract class ConfiguratorBase<TImplementation> : IConfigurator<TImplementation>
	{
		/// <summary>
		/// Create a <see cref="ConfiguratorBase{TImplementation}"/> for configuring Dependency Injection for the type <typeparamref name="TImplementation"/>.
		/// </summary>
		/// <param name="name">Optional name to use for configuration. <seealso cref="Name"/></param>
		protected ConfiguratorBase(string name = null)
		{
			Name = name;
		}

		/// <summary>
		/// <see cref="IServiceLocator"/>
		/// </summary>
		public IServiceLocator ServiceLocator { protected internal get; set; }

		/// <summary>
		/// <see cref="IConfigurator{TImplementation}.Name"/>
		/// </summary>
		public string Name { get; protected internal set; }

		/// <summary>
		/// <see cref="IConfigurator{TImplementation}.AsSingleton()"/>
		/// </summary>
		public abstract IConfigurator<TImplementation> AsSingleton();

		/// <summary>
		/// Implement this to enable the <see cref="InjectionConstructor(Expression{Func{TImplementation}},string[])"/> method to call this adapted simplified version.
		/// </summary>
		/// <param name="constructionExpressions">The (<see cref="String"/>)key/(<see cref="Type"/>) pairs that make up constructor parameters types and optional names for the implementations.</param>
		/// <returns>this</returns>
		protected abstract IConfigurator<TImplementation> InjectionConstructor(params KeyedType[] constructionExpressions);

		/// <summary>
		/// Implement this to enable the <see cref="InjectionProperties(Expression{Func{TImplementation,object}}[])"/> method to call this adapted simplified version.
		/// </summary>
		/// <param name="keyedPropertyInjectionPropertyExpressions">The (<see cref="String"/>)key/(<see cref="String"/>Name) pairs that make up property names and optional keys.</param>
		/// <returns>this</returns>
		protected abstract IConfigurator<TImplementation> InjectionProperties(params KeyedName[] keyedPropertyInjectionPropertyExpressions);

		/// <summary>
		/// Use an <see cref="Expression{T}"/> to define a <see cref="Func{TImplementation}"/> for creating an instance of <typeparamref name="TImplementation"/>.
		/// <seealso cref="IConfigurator{TImplementation}.InjectionConstructor(Expression{Func{TImplementation}},string[])"/>
		/// </summary>
		/// <remarks>The <see cref="Func{TImplementation}"/> has to be a <see cref="NewExpression"/></remarks>
		public IConfigurator<TImplementation> InjectionConstructor(Expression<Func<TImplementation>> constructionExpression,
																															 params string[] sequentialImplementationKeys)
		{
			if (constructionExpression == null) return InjectionConstructor(Enumerable.Empty<KeyedType>().ToArray());

			var lambda = ExpectType<LambdaExpression>(constructionExpression);
			var newExpression = ExpectType<NewExpression>(lambda.Body);

			var constructorParameterValues = newExpression.Arguments
																										.Select(argument => argument.Type);

			if (!constructorParameterValues.Any() || sequentialImplementationKeys.IsNullOrEmpty())
				return InjectionConstructor(constructorParameterValues.Select(cpv => new KeyedType(cpv)).ToArray());

			var constructorParameterValuesArray = constructorParameterValues.ToArray();
			var keyedTypesArray = new KeyedType[constructorParameterValuesArray.Length];

			for (var i = 0; i < keyedTypesArray.Length; i++)
			{
				var key = sequentialImplementationKeys.Length - 1 >= i ?
									sequentialImplementationKeys[i] :
									null;
				keyedTypesArray[i] = new KeyedType(constructorParameterValuesArray[i], key);
			}

			return InjectionConstructor(keyedTypesArray);
		}

		/// <summary>
		/// Use <see cref="Expression{T}"/>s to define <see cref="Func{TImplementation,Object}"/> for properties on <typeparamref name="TImplementation"/>.
		/// <seealso cref="IConfigurator{TImplementation}.InjectionProperties(Expression{Func{TImplementation,object}}[])"/>
		/// </summary>
		/// <remarks>The <see cref="Func{TImplementation,Object}"/> has to be <see cref="MemberExpression"/>s</remarks>
		public IConfigurator<TImplementation> InjectionProperties(params Expression<Func<TImplementation, object>>[] injectionPropertyExpressions)
		{
			if (injectionPropertyExpressions.IsNullOrEmpty()) return this;

			var injectionPropertyNames = from expression in injectionPropertyExpressions
																	 let propertyName = ExtractPropertyName(expression)
																	 select new KeyedName(propertyName);

			return InjectionProperties(injectionPropertyNames.ToArray());
		}

		/// <summary>
		///
		/// </summary>
		/// <param name="keyedPropertyInjectionPropertyExpressions"></param>
		/// <returns></returns>
		public IConfigurator<TImplementation> InjectionProperties(params KeyedPropertyExpression<TImplementation>[] keyedPropertyInjectionPropertyExpressions)
		{
			if (keyedPropertyInjectionPropertyExpressions.IsNullOrEmpty()) return this;

			var injectionPropertyNames = from expressionNamed in keyedPropertyInjectionPropertyExpressions
																	 let propertyName = ExtractPropertyName(expressionNamed.InjectionPropertyExpression)
																	 select new KeyedName(propertyName, expressionNamed.ImplementationKey);

			return InjectionProperties(injectionPropertyNames.ToArray());
		}

		private static string ExtractPropertyName(Expression expression)
		{
			var lambda = ExpectType<LambdaExpression>(expression);
			var memberExpression = ExpectType<MemberExpression>(lambda.Body);
			var property = ExpectType<PropertyInfo>(memberExpression.Member);
			return property.Name;
		}

		private static T ExpectType<T>(object o) where T : class
		{
			var t = o as T;
			if (t == null) throw new MalformedConfigurationExpressionException();
			return t;
		}
	}
}